package com.dbflow5.query.list;

import com.dbflow5.adapter.RetrievalAdapter;
import com.dbflow5.config.FlowLog;
import com.dbflow5.config.FlowManager;
import com.dbflow5.database.DatabaseWrapper;
import com.dbflow5.database.FlowCursor;
import com.dbflow5.query.ModelQueriable;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;

/**
 * Interface for callbacks when cursor gets refreshed.
 */
//typealias OnCursorRefreshListener<T> = (cursorList: FlowCursorList<T>) -> Unit

/**
 * Description: A non-modifiable, cursor-backed list that you can use in [ListContainer] or other data sources.
 */
public class FlowCursorList<T> implements IFlowCursorIterator<T> {

    Class<T> table;
    ModelQueriable<T> modelQueriable;
    private FlowCursor _cursor = null;
    private final Function<Void, FlowCursor> cursorFunc;
    DatabaseWrapper databaseWrapper;

    private boolean trackingCursor;

    RetrievalAdapter<T> instanceAdapter;

    private final Set<Function<FlowCursorList<T>, Void>> cursorRefreshListenerSet = new HashSet<>();

    private FlowCursorList(Builder<T> builder) {
        table = builder.modelClass;
        this.modelQueriable = builder.modelQueriable;
        this.databaseWrapper = builder.databaseWrapper;
        trackingCursor = builder.cursor != null;
        cursorFunc = unused -> {
            if(builder.cursor != null){
                return builder.cursor;
            }else {
                if(modelQueriable.cursor(databaseWrapper) != null){
                    return modelQueriable.cursor(databaseWrapper);
                }else {
                    throw new IllegalStateException("The object must evaluate to a cursor");
                }
            }
        };
        instanceAdapter = FlowManager.getRetrievalAdapter(builder.modelClass);
    }

    @Override
    public boolean trackingCursor() {
        return trackingCursor;
    }

    /**
     * The full, converted [T] list from the database on this list.
     * @return the full, converted [T] list from the database on this list. For large
     * data sets that require a large conversion, consider calling this on a BG thread.
     */
    public List<T> all() {
        unpackCursor();
        throwIfCursorClosed();
        warnEmptyCursor();

        List<T> list = instanceAdapter.getListModelLoader().convertToData(_cursor, databaseWrapper);
        if(list == null){
            list = new ArrayList<>();
        }
        return list;
    }

    /**
     * The count of rows on this database query list.
     * @return the count of rows on this database query list.
     */
    public boolean isEmpty() {
        throwIfCursorClosed();
        warnEmptyCursor();
        return count() == 0L ;
    }

    @Override
    public boolean isClosed() {
        return cursor().isClosed();
    }

    @Override
    public FlowCursorIterator<T> iterator() {
        return new FlowCursorIterator<>(databaseWrapper, this);
    }

    @Override
    public FlowCursorIterator<T> iterator(long startingLocation, long limit) {
        return new FlowCursorIterator<>(databaseWrapper, this, startingLocation, limit);
    }


    /**
     * Register listener for when cursor refreshes.
     * @param onCursorRefreshListener onCursorRefreshListener
     */
    public void addOnCursorRefreshListener(Function<FlowCursorList<T>, Void> onCursorRefreshListener) {
        synchronized(cursorRefreshListenerSet) {
            cursorRefreshListenerSet.add(onCursorRefreshListener);
        }
    }

    public void removeOnCursorRefreshListener(Function<FlowCursorList<T>, Void> onCursorRefreshListener) {
        synchronized(cursorRefreshListenerSet) {
            cursorRefreshListenerSet.remove(onCursorRefreshListener);
        }
    }

    /**
     * Refreshes the data backing this list, and destroys the Model cache.
     */
    public synchronized void refresh() {
        FlowCursor cursor = unpackCursor();
        cursor.close();
        this._cursor = modelQueriable.cursor(databaseWrapper);
        trackingCursor = false;
        synchronized(cursorRefreshListenerSet) {
            for(Function<FlowCursorList<T>, Void> listener : cursorRefreshListenerSet){
                listener.apply(this);
            }
        }
    }

    /**
     * Returns a model at the specified index. If we are using the cache and it does not contain a model
     * at that index, we move the cursor to the specified index and construct the [T].
     *
     * @param index The row number in the [FlowCursor] to look at
     * @return The [T] converted from the cursor
     */
    @Override
    public T get(long index) {
        throwIfCursorClosed();

        FlowCursor cursor = unpackCursor();
        if (cursor.goToRow((int)index)) {
            T model = instanceAdapter.getSingleModelLoader().convertToData(FlowCursor.from(cursor), false, databaseWrapper);
            if(model != null){
                return model;
            }else {
                throw new IndexOutOfBoundsException("Invalid item at index " + index + ". Check your cursor data.");
            }
        } else {
            throw new IndexOutOfBoundsException("Invalid item at index " + index + ". Check your cursor data.");
        }
    }

    /**
     * The count of the rows in the [FlowCursor] backed by this list.
     * @return the count of the rows in the [FlowCursor] backed by this list.
     */
    @Override
    public long count() {
        unpackCursor();
        throwIfCursorClosed();
        warnEmptyCursor();
        return _cursor.getRowCount();
    }

    /**
     * Closes the cursor backed by this list
     */
    @Override
    public void close() {
        warnEmptyCursor();
        _cursor.close();
        _cursor = null;
    }

    @Override
    public FlowCursor cursor() {
        unpackCursor();
        throwIfCursorClosed();
        warnEmptyCursor();
        return _cursor;
    }

    private FlowCursor unpackCursor() {
        if(_cursor == null){
            _cursor = cursorFunc.apply(null);
        }
        return _cursor;
    }

    private void throwIfCursorClosed() {
        if (_cursor.isClosed()) {
            throw new IllegalStateException("Cursor has been closed for FlowCursorList");
        }
    }

    private void warnEmptyCursor() {
        if (_cursor == null) {
            FlowLog.log(FlowLog.Level.W, "Cursor was null for FlowCursorList");
        }
    }

    /**
     * A new [Builder] that contains the same cache
     * @return A new [Builder] that contains the same cache, query statement, and other
     * underlying data, but allows for modification.
     */
    public Builder<T> newBuilder() {
        return new Builder<>(modelQueriable, databaseWrapper).cursor(_cursor);
    }

    /**
     * Provides easy way to construct a [FlowCursorList].
     */
    public static class Builder<T> {
        ModelQueriable<T> modelQueriable;
        DatabaseWrapper databaseWrapper;
        Class<T> modelClass;
        FlowCursor cursor = null;

        public Builder(ModelQueriable<T> modelQueriable, DatabaseWrapper databaseWrapper){
            this.modelQueriable = modelQueriable;
            this.databaseWrapper = databaseWrapper;
            modelClass = modelQueriable.table();
        }

        Builder<T> cursor(FlowCursor cursor) {
            this.cursor = cursor;
            return this;
        }

        public FlowCursorList<T> build() {
            return new FlowCursorList<>(this);
        }

    }

}
